from django.utils import timezone
from django.db import transaction
import numpy as np
import pandas as pd
import random
from utils.IOService import get_product, get_inventory_filter

class PurchaseSim:
    def __init__(self):
        self.demand_ctg_cnt = random.choice([1,2,3])
        self.demand_prd_pool = self.product_demand_pool()
        self.consumer = None

    def product_demand_pool(self):
        # 获取基础商品信息
        prd_df = get_product()
        
        # 随机选择类别
        ctg_choice = np.random.choice(prd_df['category_code'].unique().tolist(), size=self.demand_ctg_cnt, replace=False)
        prd_demand_df = prd_df[prd_df['category_code'].isin(ctg_choice)]
        prd_interest_df = prd_df[~prd_df['category_code'].isin(ctg_choice)]
        
        # 合并需求商品池
        prd_demand_pool = pd.concat([prd_demand_df, prd_interest_df.sample(100)])
        
        # 获取商品库存信息并关联
        inventory_df = get_inventory_filter(prd_demand_pool['product_code'].tolist())[['product_code', 'instock_quantity']]
        prd_demand_pool = prd_demand_pool.merge(
            inventory_df,
            on='product_code',
            how='left'
        )
        
        return prd_demand_pool

    def purchase(self):
        # 实例化一个客户，让客户自己决定预算
        client = ClientGen()
        prd_buying_list = client.purchase(self.demand_prd_pool)
        self.consumer = client
        
        # 如果购物列表为空，返回空的DataFrame
        if not prd_buying_list:
            return pd.DataFrame()
            
        # 直接将购物列表转换为DataFrame
        return pd.DataFrame(prd_buying_list)

    def insert_purchase_data(self, purchase_df):
        """
        将购物数据插入到数据库中
        :param purchase_df: pandas DataFrame 购物数据
        """
        from events.models import StoreSalesOrder, StoreSalesOrderDetail
        import uuid

        with transaction.atomic():
            try:
                # 生成订单号
                order_id = str(uuid.uuid4())

                # 使用 timezone.now() 替代 datetime.now()
                finished_at = timezone.localtime()
                
                # 计算订单总价
                total_price = purchase_df['default_retail_price'].sum()
                
                # 创建订单主表记录
                order = StoreSalesOrder.objects.create(
                    order_id=order_id,
                    order_date=finished_at.date(),
                    store_code='001',
                    retail_price=total_price,
                    transaction_price=total_price,
                    finished_at=finished_at
                )
                
                # 计算每个商品的购买数量
                product_quantities = purchase_df.groupby('product_code').size().reset_index(name='quantity')
                
                # 创建订单明细记录
                order_details = []
                for _, row in product_quantities.iterrows():
                    detail = StoreSalesOrderDetail(
                        order_id=order_id,
                        product_code=row['product_code'],
                        quantity=row['quantity']
                    )
                    order_details.append(detail)
                
                # 批量创建订单明细
                StoreSalesOrderDetail.objects.bulk_create(order_details)
                
                # 调用库存扣减方法
                self.update_inventory_reduce(product_quantities)
                
                print(f"订单 {order_id} 创建成功")
                return True
                
            except Exception as e:
                print(f"插入数据失败: {str(e)}")
                return False

    def update_inventory_reduce(self, product_quantities):
        """
        更新商品库存（扣减）- 真正的批量更新版本
        :param product_quantities: DataFrame 包含 product_code 和 quantity 的商品数量信息
        :raises: ValueError 当库存不足时抛出异常
        """
        from product.models import ProductsInventory
        from django.db.models import F
        from django.db.models import Case, When

        with transaction.atomic():
            try:
                # 1. 批量获取所有相关商品的库存信息
                product_codes = product_quantities['product_code'].tolist()
                inventories = ProductsInventory.objects.filter(
                    product_code__in=product_codes
                ).values('product_code', 'instock_quantity')
                
                # 2. 转换为字典方便查找
                inventory_dict = {inv['product_code']: inv['instock_quantity'] for inv in inventories}
                
                # 3. 批量检查库存是否足够
                for _, row in product_quantities.iterrows():
                    current_stock = inventory_dict.get(row['product_code'])
                    if current_stock is None or current_stock < row['quantity']:
                        raise ValueError(
                            f"商品 {row['product_code']} 库存不足，当前库存: {current_stock or 0}, 需求数量: {row['quantity']}"
                        )
                
                # 4. 构建批量更新条件
                update_time = timezone.localtime()
                case_whens = []
                for product_code, quantity in zip(product_quantities['product_code'], product_quantities['quantity']):
                    case_whens.append(When(product_code=product_code, then=F('instock_quantity') - quantity))
                
                # 5. 执行真正的批量更新
                ProductsInventory.objects.filter(
                    product_code__in=product_codes
                ).update(
                    instock_quantity=Case(*case_whens),
                    updated_at=update_time
                )
                
                return True
                
            except Exception as e:
                print(f"库存扣减失败: {str(e)}")
                raise  # 抛出异常以触发事务回滚






class ClientGen:
    def __init__(self, budget=None, initial_energy=random.randint(30, 60),
                 k_range=(0.5, 2), alpha_range=(1, 3), beta_range=(2, 4),
                 decision_point_range=(0.45, 0.65), min_budget=50, max_budget=1000):
        # 使用对数正态分布和本福特定律生成预算
        if budget is None:
            # 本福特定律的首位数字概率分布
            benford_probs = {
                1: 0.301, 2: 0.176, 3: 0.125, 4: 0.097,
                5: 0.079, 6: 0.067, 7: 0.058, 8: 0.051, 9: 0.046
            }
            
            # 根据本福特定律选择首位数字
            first_digit = np.random.choice(
                list(benford_probs.keys()),
                p=list(benford_probs.values())
            )
            
            # 使用对数正态分布生成基础预算
            mu = np.log(100)  # 均值取ln(200)，使预算集中在较合理的区间
            sigma = 0.8       # 标准差控制预算分布的分散程度
            
            # 生成基础预算
            base_budget = np.random.lognormal(mu, sigma)
            
            # 调整预算以符合本福特定律的首位数字
            magnitude = int(np.log10(base_budget))
            budget = first_digit * (10 ** magnitude)
            
            # 确保预算在指定范围内
            self.budget = max(min_budget, min(max_budget, budget))
        else:
            self.budget = budget
            
        self.cost = 0
        self.basic_decide_point = random.uniform(*decision_point_range)
        self.K = random.uniform(*k_range)
        self.ALPHA = random.uniform(*alpha_range)
        self.BETA = random.uniform(*beta_range)
        self.energy = initial_energy
        self.blanket = []
        self.max_item_quantity = random.randint(1, 5)  # 添加单品最大购买数量限制
        self.purchased_quantities = {}  # 记录已购买的商品数量
        
        """
        P(购买) = e^(-k * (价格/预算)) * (1 - 已花费/预算)^α * (1 + (1 - 剩余体力比例)^β)
        这个是虚拟顾客的购买概率模型
        - k 是价格敏感度系数（建议取值 0.5-2）
        - α 是预算约束系数（建议取值 1-3）
        - 已花费 是顾客在当前购物过程中已经花费的金额
        - 剩余体力比例 = 剩余体力 / 初始体力
        - β 是体力紧迫度系数（建议取值 2-4）
        """

    @property
    def remaining_budget(self):
        return self.budget - self.cost

    @property
    def energy_ratio(self):
        return self.energy / 100

    @property
    def is_exhausted(self):
        return self.energy <= 0 or self.cost >= self.budget

    def decide(self, product):
        if product['default_retail_price'] > self.remaining_budget:
            return None
            
        # 直接检查商品的库存字段
        if not product['instock_quantity'] or product['instock_quantity'] <= 0:
            return None
            
        # 检查是否达到最大购买数量
        current_quantity = self.purchased_quantities.get(product['product_code'], 0)
        if current_quantity >= self.max_item_quantity:
            return None
            
        possible = (np.exp(-self.K * (product['default_retail_price']/self.budget)) *
                    ((1 - self.cost/self.budget) ** self.ALPHA) * 
                    (1 + (1 - self.energy_ratio) ** self.BETA))
        
        self.energy = max(0, self.energy - 1)  # 防止能量变成负数
        
        # 使用sigmoid函数动态调整购买概率
        """
        - 概率增加更加平滑，避免了突变
        - 当能量非常低时，增加的概率会趋近于1
        - 当能量接近0.4时，增加的概率会平滑地趋近于0
        - sigmoid函数的特性保证了概率增加的合理性和平滑性
        """
        if self.energy_ratio < 0.4:
            energy_factor = 1 /(2* (1 + np.exp(5 * (self.energy_ratio - 0.2))))  # sigmoid函数
            possible += energy_factor
            
        return product if possible >= self.basic_decide_point else None
    def purchase(self, products):
        """
        模拟购物过程，允许多次选择同一商品，使用sales_point作为选择权重
        """
        # 将商品转换为列表以支持多次遍历
        product_list = products.to_dict('records')
        
        # 添加最大尝试次数限制
        max_attempts = len(product_list) * 3  # 设置一个合理的最大尝试次数
        attempts = 0
        
        while not self.is_exhausted:
            # 如果没有可选商品或达到最大尝试次数，退出循环
            if not product_list or attempts >= max_attempts:
                break
                
            attempts += 1
            
            # 使用sales_point作为权重进行随机选择
            weights = [p.get('sales_point', 0.5) for p in product_list]  # 默认权重0.5
            weights = np.array(weights)  # 转换为numpy数组
            product = np.random.choice(product_list, p=weights/np.sum(weights))  # 归一化权重
            # product = np.random.choice(product_list, p=weights)  # 归一化权重

            buying = self.decide(product)
            
            if buying is not None:
                self.cost += product['default_retail_price']
                self.blanket.append(product)
                
                # 更新已购买数量
                product_code = product['product_code']
                self.purchased_quantities[product_code] = self.purchased_quantities.get(product_code, 0) + 1
                
                # 如果达到最大购买数量，从可选列表中移除
                if self.purchased_quantities[product_code] >= self.max_item_quantity:
                    product_list = [p for p in product_list if p['product_code'] != product_code]
                    
                # 重置尝试次数（成功购买后）
                attempts = 0
        
        return self.blanket

